/**
 * 
 */
package com.googlecode.lohr.parser.combinators.matcher;


import java.util.ArrayList;
import java.util.List;

import com.googlecode.lohr.parser.combinators.MatchError;
import com.googlecode.lohr.parser.combinators.Parsers;
import com.googlecode.lohr.parser.combinators.node.AbstractNode;
import com.googlecode.lohr.parser.combinators.node.RepetitionNode;

public final class RepetitionMatcher extends Matcher
{
	public enum Type
	{
		SEQUENCE, CHOICE
	}

	private final int _min;
	private final int _max;
	private final Matcher _matcher;

	public RepetitionMatcher(int min, int max, Type type, Matcher... matchers)
	{
		this._min = min;
		this._max = max;

		if (1 < matchers.length) {
			switch (type)
			{
			case SEQUENCE:
				this._matcher = Parsers.seq(matchers);
				break;
			case CHOICE:
				this._matcher = Parsers.choice(matchers);
				break;
			default:
				throw new RuntimeException("Unknown repetition matcher subtype: " + type);
			}
		}
		else
			_matcher= matchers[0];
	}

	public RepetitionMatcher(int min, int max, Matcher matcher)
	{
		this._min = min;
		this._max = max;
		this._matcher = matcher;
	}

	@Override
	public void match(MatchContext ctx, int start) 
	{
		final Matcher repMatcher= this;
		final int repStart= start;
		final ArrayList<AbstractNode> _matches= new ArrayList<AbstractNode>();
		
		// task for repeating a match until no match found or max count is reached.
		class MaxTask extends MatchTask {
			AbstractNode prevMatch;
			int repcount;
			
			MaxTask(MatchContext ctx, AbstractNode prevMatch, int repcount) {
				super(ctx, _matcher, (prevMatch != null) ? prevMatch.getEnd() : repStart);
				this.prevMatch= prevMatch;
				this.repcount= repcount;
			}
			public void onMatchFound(AbstractNode match) {
				_matches.add(match);
				AbstractNode combinedMatch= new RepetitionNode(repMatcher, repStart, match.getEnd());
				combinedMatch.setChildren(_matches);
				if (repcount < _max) {
					// if we haven't reached the max limit then look for another match
					new MaxTask(ctx, combinedMatch, repcount+1).run();
				}
				else {
					// we reached the max limit, just return what we got
					ctx.matchFound(repMatcher, repStart, combinedMatch);
				}
			};
			public void onMatchComplete(MatchError matchError) {
				// this is the end of the line
				if (matchError != null || _max <= repcount) {
					
					boolean hasMatches= ctx.hasMatches(repMatcher, repStart);
					
					// if this search found no matches then return the results 
					// from the previous search
					if (!hasMatches && prevMatch != null) {
						ctx.matchFound(repMatcher, repStart, prevMatch);
						matchError= null;
					}
					
					// optional repetitions should produce match of zero length
					else if (!hasMatches && repcount <= 0 && _min <= 0) {
						ctx.matchFound(repMatcher, repStart, new RepetitionNode(repMatcher, repStart, repStart));
						matchError= null;
					}
					
					if (_min <= 0)
						matchError= null;
					
					ctx.matchComplete(repMatcher, repStart, matchError);
				}
			}
		};

		// create a matcher that will match the minimum required # of repetitions
		List<Matcher> repetition = new ArrayList<Matcher>();
		for (int i = 0; i < _min; i++) { repetition.add(_matcher); }
		final Matcher minMatcher= Parsers.seq(repetition.toArray(new Matcher[repetition.size()]));

		if (0 < _min) {
			new MatchTask(ctx, minMatcher, start) {
				@Override
				public void onMatchFound(AbstractNode match) {
					if (_min < _max) {
						_matches.addAll(match.getChildren());
						new MaxTask(ctx, match, _min).run();
					}
					else {
						RepetitionNode node= new RepetitionNode(repMatcher, repStart, match.getEnd());
						node.setChildren(match.getChildren());
						ctx.matchFound(repMatcher, repStart, node);
					}
				}
				@Override
				public void onMatchComplete(MatchError matchError) {
					if (_max <= _min || !ctx.hasMatches(minMatcher, start))
						ctx.matchComplete(repMatcher, repStart, matchError); 
				}
			}.run();
		}
		else
			new MaxTask(ctx, null, 0).run();
	}
}